#include <stdio.h>
#include <sys/types.h>       
#include <sys/socket.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <strings.h>
#include <unistd.h>

#include "bloom_filter.h"
#include "util.h"

static FILE * config_file = NULL;

static char line_buf[2048]; 
static char read_buf[2048]; 
static int reread_buf = 0; 
bloom_filter_t blank_bf = {0};

// Config parameters:
u_int32_t sessionid = 0;
u_int32_t target_addr = 0; 
int target_port = 0;
bloom_filter_t packet_bloom_filter = {0};
int packet_length = 0;
int packets_per_second = 0;
int duration = 0;

/**
* Read a non-empty and non-comment line from a File. Return NULL if 
* error occurs or end-of-file.
*/

char * read_line() 
{ 
    int len; 
    if (reread_buf) { 
        reread_buf = 0; 
        strcpy(line_buf, read_buf); 
        return line_buf; 
    }

    while(fgets(read_buf, sizeof(read_buf), config_file) != NULL) { 
        if (read_buf[0] == '\0' || read_buf[0] == '#' || read_buf[0] == ' ' || read_buf[0] == '\t' || read_buf[0] == '\n') { 
            continue; 
        }
        len = strlen(read_buf); 
        if (read_buf[len-1] == '\n') { 
            read_buf[len-1] = '\0'; 
        }

        strcpy(line_buf, read_buf); 
        return line_buf; 
    }
    return NULL; 
}

void unread_line() 
{
    reread_buf = 1; 
} 

// Parse the configuration file.  0 on success; -1 on failure.
int ParseConfigFile()
{
    /*
        session: xx
        address: xxx.xxx.xxx.xxx
        port: xxxx
        bloomfilter: xxxxxxxxxxxxxxxx
        packetlength: xxxx
        packet/s: xx
        duration: xxx
    */

    char * line;
    while ((line = read_line()) != NULL)
    {
        if (strstr(line, "session: ") != NULL)
        {
            sessionid = atoi(line + sizeof("session:"));
        }
        if (strstr(line, "address: ") != NULL)
        {
            target_addr = inet_addr(line + sizeof("address:"));
        }
        if (strstr(line, "port: ") != NULL)
        {
            target_port = atoi(line + sizeof("port:"));
        }
        if (strstr(line, "bloomfilter: ") != NULL)
        {
            hex_string_to_bloom_filter(line + sizeof("bloomfilter:"), packet_bloom_filter);
        }
        if (strstr(line, "packetlength: ") != NULL)
        {
            packet_length = atoi(line + sizeof("packetlength:"));
        }
        if (strstr(line, "packet/s: ") != NULL)
        {
            packets_per_second = atoi(line + sizeof("packet/s:"));
        }
        if (strstr(line, "duration: ") != NULL)
        {
            duration = atoi(line + sizeof("duration:"));
        }

        // Got to the next host
        if (strstr(line, "host: ") != NULL)
        {
            break;
        }

        // Or if we reach EOF, this will also break.
    }

    printf("sessionid: %d\n\
            target_addr: %s\n\
            target_port: %d\n\
            packet_bloom_filter: %s\n\
            packet_length: %d\n\
            packets_per_second: %d\n\
            duration: %d\n",
            sessionid,
            ip_to_string(htonl(target_addr)),
            target_port,
            bloom_filter_to_string(packet_bloom_filter),
            packet_length,
            packets_per_second,
            duration);

    if ((sessionid == 0) ||
        (target_addr == 0) ||
        (target_port == 0) ||
        (memcmp(packet_bloom_filter, blank_bf, sizeof(bloom_filter_t)) == 0) ||
        (packet_length == 0) ||
        (packets_per_second == 0) ||
        (duration == 0))
    {
        return -1;
    }

    return 0;
}

void parse_host() 
{
    struct in_addr in; 
    char * line; 
    char *token; 

    u_int32_t local_ip = get_local_ip_address();     
    while((line = read_line()) != NULL) {      
        char * token = strsep(&line, ":=");
        if (strcmp(token, "host") == 0) { 
            // Check whether this host is the equivalent to the local host
            char * addr = line; 
            // skip white spaces
            while( (*addr == ' ') || (*addr == '\t') ) { 
                addr++; 
            }
            if (inet_aton(addr, &in) == 0) { 
                perror("Error when converting network address"); 
                exit(-1); 
            }
            if (ntohl(in.s_addr) != local_ip) { 
                continue; 
            }
            if (ParseConfigFile() != 0)
            {
                perror("Error parsing config file");
                exit(-1);
            }

            return; 
        }
    }    
    fprintf(stderr, "No host entry found for this host, check your configuration file\n"); 
    exit(-1); 
}

main(int argc, char **argv) 
{
    if ((argc != 2) && (argc != 8))
    {
        printf("Usage: %s <ConfigFile>\n", argv[0]);
        printf("    OR %s SessionID Address Port BloomFilter Length Rate Duration\n", argv[0]);
        exit(1);
    } 

    if (argc == 2)
    {
        // Process the config file
        config_file = fopen(argv[1], "r");
        if (config_file == NULL)
        {
            perror("config file not found.\n");
            exit(-1);
        }

        // Parse configuration file
        parse_host();
    }
    else if (argc == 8)
    {
        // Process via command line
        sessionid = atoi(argv[1]);
        target_addr = inet_addr(argv[2]);
        target_port = atoi(argv[3]);
        hex_string_to_bloom_filter(argv[4], packet_bloom_filter);
        packet_length = atoi(argv[5]);
        packets_per_second = atoi(argv[6]);
        duration = atoi(argv[7]);

        printf("sessionid: %d\n\
               target_addr: %s\n\
               target_port: %d\n\
               packet_bloom_filter: %s\n\
               packet_length: %d\n\
               packets_per_second: %d\n\
               duration: %d\n",
               sessionid,
               ip_to_string(htonl(target_addr)),
               target_port,
               bloom_filter_to_string(packet_bloom_filter),
               packet_length,
               packets_per_second,
               duration);


        if ((sessionid == 0) ||
            (target_addr == 0) ||
            (target_port == 0) ||
            (memcmp(packet_bloom_filter, blank_bf, sizeof(bloom_filter_t)) == 0) ||
            (packet_length == 0) ||
            (packets_per_second == 0) ||
            (duration == 0))
        {
            perror("invalid parameters.\n");
            exit(-1);
        }
    }

    // Construct socket
    int sockfd = socket(PF_INET, SOCK_DGRAM, 0); 
    if (sockfd < 0) 
    { 
        perror("Error when creating socket"); 
        exit(1); 
    }

    // Handle multicast options
    if (MULTICAST(target_addr))
    {
        u_char loop = 1;
        if (setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_LOOP, &loop, sizeof(loop)) < 0)
        {
            perror("Error when setsockopt IP_MULTICAST_LOOP");
            exit(1);
        }

        static struct ip_mreq mreq; 
        mreq.imr_multiaddr.s_addr = target_addr; 
        mreq.imr_interface.s_addr = INADDR_ANY; 
        if (setsockopt(sockfd, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mreq, sizeof(mreq)) < 0)
        { 
            perror("Error when setsockopt"); 
            exit(1); 
        }

        const int TTL=64;
        if (setsockopt(sockfd, IPPROTO_IP, IP_MULTICAST_TTL, &TTL, sizeof(TTL)) < 0)
        { 
            perror("Error when setsockopt"); 
            exit(1); 
        }
    }

    ssize_t msglen; 
    socklen_t addrlen; 

    struct sockaddr_in dst_addr; 
    memset(&dst_addr, 0, sizeof(dst_addr)); 
    dst_addr.sin_family = AF_INET; 
    dst_addr.sin_addr.s_addr = target_addr; 
    dst_addr.sin_port = htons(target_port);

    char * buf = malloc(packet_length); 
    memset(buf, 0, packet_length);
    char * tail = buf;

    struct timeval start, expire, now; 
    gettimeofday(&start, NULL);
    expire.tv_sec = start.tv_sec + duration;
    expire.tv_usec = start.tv_usec;

    long increment = 1000000 / packets_per_second;   // How many microseconds to spend per packet.
    int totalToSend = duration * packets_per_second;

    // Populate with packet BloomFilter
    copy_bloom_filter(tail, packet_bloom_filter);
    tail += sizeof(bloom_filter_t);
    // then with session id
    u_int32_t * pSessionID = (u_int32_t *) tail;
    *pSessionID = sessionid;
    tail += sizeof(u_int32_t);
    // and the number of packets to expect from this session
    int * pTotalToSend = (int *) tail;
    *pTotalToSend = totalToSend;
    tail += sizeof(int);

    int i = 0;
    for (i = 0; i < totalToSend; i++)
    { 
        // and the sequence number of this packet (this session)
        int * pSequenceNumber = (int *) tail;
        *pSequenceNumber = i + 1;

        msglen = sendto(sockfd, buf, packet_length, 0, &dst_addr, sizeof(dst_addr) );
        if (msglen < 0) { 
            perror("Error when send"); 
            continue; 
        }

        // TODO: latency sendback.

        gettimeofday(&now, NULL);

        // ctime includes trailing endline.
        printf("Session %d - Sent to %s:%d - %d bytes - %d of %d - %s",
            sessionid, ip_to_string(htonl(target_addr)), target_port,
            packet_length, i + 1, *pTotalToSend, ctime(&now.tv_sec));

        long elapsed = (now.tv_sec - start.tv_sec) * 1000000 + (now.tv_usec - start.tv_usec) - i * increment;
        if (increment - elapsed > 0)
            usleep(increment - elapsed); 
    }

    printf("Client completed - %d seconds elapsed - %d packets sent.\n", now.tv_sec - start.tv_sec, i);
}
